import CryptoJS from 'crypto-js'

/**
 * Triple DES 加密模式名称枚举
 */
export const enum TripleDesModeEnum {
  ECB = 'ECB',
  CBC = 'CBC',
  CFB = 'CFB',
  CTR = 'CTR',
  OFB = 'OFB',
  CTRGladman = 'CTRGladman',
}

/**
 * Triple DES 加密填充方式名称枚举
 */
export const enum TripleDesPaddingEnum {
  AnsiX923 = 'AnsiX923',
  Iso10126 = 'Iso10126',
  ZeroPadding = 'ZeroPadding',
  Pkcs7 = 'Pkcs7',
  NoPadding = 'NoPadding',
  Iso97971 = 'Iso97971',
}

/**
 * Triple DES 加密工具中会使用到的常量
 */
export const TripleDesConst = {
  /**
   * 在后端中，DES 加密算法的密钥限制为长度 24 位的 byte 数组
   */
  KEY_LEN: 24,
  /**
   * 在后端中，DES 加密算法的加密盐(也就是后面的偏移量 IV)限制为长度 8 位的 byte 数组
   */
  SALT_LEN: 8,
  /**
   * 由于在后端中，DES 加密算法的默认实现为 DESede/ECB/PKCS5Padding，所以默认的加密模式为 ECB
   */
  MODE_DEFAULT: TripleDesModeEnum.ECB,
  /**
   * 在后端中，DES 加密算法的默认实现为 DESede/ECB/PKCS5Padding，但是由于 CryptoJS 中没有与之
   * 对应的最后一个分组的填充方式 padding，所以这里使用前后端都有的最后一个分组的填充方式 ZeroPadding
   */
  PADDING_DEFAULT: TripleDesPaddingEnum.ZeroPadding,
}

/**
 * Triple DES 加密算法加密模式
 */
export const TripleDesMode = {
  ECB: CryptoJS.mode.ECB,
  CBC: CryptoJS.mode.CBC,
  CFB: CryptoJS.mode.CFB,
  CTR: CryptoJS.mode.CTR,
  OFB: CryptoJS.mode.OFB,
  CTRGladman: CryptoJS.mode.CTRGladman,
} as const

/**
 * Triple DES 加密算法加密模式可选模式的类型
 */
export type TripleDesModeType = keyof typeof TripleDesMode

/**
 * Triple DES 加密算法最后一个分组的填充方式
 */
export const TripleDesPadding = {
  AnsiX923: CryptoJS.pad.AnsiX923,
  Iso10126: CryptoJS.pad.Iso10126,
  ZeroPadding: CryptoJS.pad.ZeroPadding,
  Pkcs7: CryptoJS.pad.Pkcs7,
  NoPadding: CryptoJS.pad.NoPadding,
  Iso97971: CryptoJS.pad.Iso97971,
} as const

/**
 * DES 加密算法最后一个分组的填充方式可选值的类型
 */
export type TripleDesPaddingType = keyof typeof TripleDesPadding

/**
 * 判断是否需要进行后续的 DES 加密算法加密解密处理
 *
 * @param data 需要进行加密解密处理的数据
 * @param key 加密密钥
 * @param isAllType 是否对所有的数据类型进行加密解密
 * @return {boolean} true 表示需要进行加密解密处理，false 表示不需要进行加密解密处理
 */
const isDes = (data: any, key: string, isAllType: boolean): boolean => {
  // 如果需要加密解密处理的数据为空
  if (data === null || data === undefined) return false
  // 如果只对字符串类型的数据进行加密解密，但是传递进来的不是字符串类型
  if (!isAllType && typeof data !== 'string') return false
  // 如果需要加密解密处理的数据为空字符串
  if (data === '') return false
  // 加密密钥长度不足
  if (key.length < TripleDesConst.KEY_LEN) return false
  return true
}

/**
 * 处理需要进行加密解密处理数据<br/>
 * 加密模式下，如果开启了对所有的数据类型进行加密，但是传递进来的数据不是字符串类型，将其转化为 JSON 字符串；<br/>
 * 解密模式下，如果开启了对所有的数据类型进行加密，则将 JSON 字符串转化为相应的对象<br/>
 *
 * @param data 需要进行加密处理的数据
 * @param isAllType 是否对所有的数据类型进行加密
 * @param isDecrypt 是否为解密模式
 */
const handleData = (data: any, isAllType: boolean, isDecrypt: boolean = false) => {
  // 加密模式下，如果开启了对所有的数据类型进行加密，但是传递进来的数据不是字符串类型，将其转化为 JSON 字符串
  if (!isDecrypt && isAllType && typeof data !== 'string') data = JSON.stringify(data)
  // 解密模式下，如果开启了对所有的数据类型进行加密，则将 JSON 字符串转化为相应的对象
  else if (isDecrypt && isAllType) {
    try {
      data = JSON.parse(data)
    } catch (err) {
      // 如果 JSON 字符串解析失败，加密成密文之前的原数据的类型就是字符串类型
      // 所以这里不需要 JSON 解析处理，直接返回解密后的数据即可
      return data
    }
  }
  return data
}

/**
 * 预处理密钥和加密盐<br/>
 * 1. 如果密钥长度大于 24 位，则截取前 24 位；<br/>
 * 2. 如果加密盐为空或长度小于 8 位，则使用密钥的前 8 位作为盐；如果加密盐长度大于 8 位，则截取前 8 位；<br/>
 *
 * @param key 加密密钥
 * @param salt 加密盐(偏移量)
 */
const preHandleKeySalt = (key: string, salt: string) => {
  // 如果密钥长度大于 24 位，则截取前 24 位
  key = key.length > TripleDesConst.KEY_LEN ? key.substring(0, TripleDesConst.KEY_LEN) : key
  // 加密盐(偏移量)
  // 如果加密盐为空或长度小于 8 位，则使用密钥的前 8 位作为盐；如果加密盐长度大于 8 位，则截取前 8 位
  salt = salt === '' ?
    key.substring(0, TripleDesConst.SALT_LEN) :
    salt.length < TripleDesConst.SALT_LEN ?
      key.substring(0, TripleDesConst.SALT_LEN) :
      salt.substring(0, TripleDesConst.SALT_LEN)
  return {
    key,
    salt
  }
}

/**
 * DES 加密算法加密解密处理函数通用形参列表类型
 */
export type CommonDesPropsType = {
  key: string,
  salt?: string,
  isAllType?: boolean
  mode?: TripleDesModeType,
  padding?: TripleDesPaddingType
}

/**
 * DES 加密算法加密函数<br/>
 *
 * @param data 需要进行加密处理的数据。<span style="color:orange;">如果需要加密处理的数据为空，
 * 或者不是字符串类型且没有开启对所有的数据类型进行加密，或者为空字符串，都将不进行加密处理，直接返回传
 * 递进来的数据；如果开启了对所有的数据类型进行加密，但是传递进来的数据不是字符串类型，将其转化为 JSON
 * 字符串，然后进行加密处理</span>
 * @param key 加密密钥。<span style="color:orange;">如果密钥长度小于 24 位，则会直接返回传递进
 * 来需要加密处理的数据；如果密钥长度大于 24 位，则会对其进行截取，只取前 24 位</span>
 * @param salt 加密盐(偏移量)。<span style="color:orange;">如果加密盐为空或长度小于 8 位，则会
 * 使用密钥的前 8 位作为盐；如果加密盐长度大于 8 位，则会对其进行截取，只取前 8 位</span>
 * @param isAllType 是否对所有的数据类型进行加密，默认为 false。<span style="color:orange;">
 * 该项开启后，对于不是字符串类型的数据，会将其转化为 JSON 字符串，然后进行加密处理；如果没有开启，对于
 * 不为字符串类型的数据，则不会进行加密处理，直接返回原数据</span>
 * @param mode 加密模式。默认为 ECB
 * @param padding 最后一个分组的填充方式。默认为 ZeroPadding
 */
export const encrypt = (
  {
    data,
    key,
    salt = '',
    isAllType = false,
    mode = TripleDesConst.MODE_DEFAULT,
    padding = TripleDesConst.PADDING_DEFAULT,
  }: {
    data: any,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
) => {
  // 如果传递进来的数据不需要后续的加密处理，直接返回原数据
  if (!isDes(data, key, isAllType as boolean)) return data
  // 处理需要进行加密处理的数据
  data = handleData(data, isAllType as boolean)
  // 预处理密钥和加密盐
  let {key: handledKey, salt: handledSalt} = preHandleKeySalt(key, salt as string)
  // 加密处理
  return CryptoJS.TripleDES.encrypt(
    data, // 需要进行加密处理的数据
    CryptoJS.enc.Utf8.parse(handledKey), // 密钥
    {
      iv: CryptoJS.enc.Utf8.parse(handledSalt), // 偏移量(加密盐)
      mode: TripleDesMode[mode], // 加密模式
      padding: TripleDesPadding[padding], // 填充方式
    }
  ).toString()
}

/**
 * 递归加密处理对象类型或者数组类型数据中的数据项
 *
 * @param item 需要加密处理的数据项
 * @param key 加密密钥
 * @param salt 加密盐(偏移量)
 * @param isAllType 是否对所有的数据类型进行加密
 * @param isRecursion 是否递归处理数据项
 * @param mode 加密模式
 * @param padding 最后一个分组的填充方式
 */
const encryptItemRecursion = (
  {
    item, key, salt, isAllType, isRecursion, mode, padding
  }: {
    item: any,
    isRecursion?: boolean,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
) => {
  // 如果数据项为 null，则直接返回
  if (item === null) return null
  // 如果是对象类型的数据，且需要递归处理
  if (typeof item === 'object' && isRecursion) {
    // 递归处理数组
    if (Array.isArray(item)) item = encryptArr({
      arr: item,
      key, salt, isAllType, isRecursion, mode, padding,
    })
    // 递归处理对象
    else item = encryptObj({
      obj: item,
      key, salt, isAllType, isRecursion, mode, padding,
    })
  } else {
    item = encrypt({
      data: item,
      key, salt, isAllType, mode, padding,
    })
  }
  return item
}

/**
 * 对数组类型的数据进行加密处理<br/>
 * 该方法会循环对数组中的每项数据进行加密处理，然后返回一个由加密后的数据项组成的新数组
 *
 * @param arr 需要加密处理的数组数据。
 * @param key 加密密钥
 * @param salt 加密盐(偏移量)
 * @param isAllType 是否对所有的数据类型进行加密，默认为 false。<span style="color:orange;">
 * 该项开启后，对于数组中不是字符串类型的每一项数据，会将其转化为 JSON 字符串，然后进行加密处理；如果
 * 没有开启，对于不为字符串类型的数据项，则不会进行加密处理，仍然原数据项</span>
 * @param isRecursion 是否递归处理数组项。默认为 false。<span style="color:orange;">如果开启了
 * 递归加密处理，则对于数组类型或者对象类型的数据项，会优先进行递归，然后再对其中的子数组项进行加密处理
 * </span>
 * @param mode 加密模式。默认为 ECB
 * @param padding 最后一个分组的填充方式。默认为 ZeroPadding
 */
export const encryptArr = (
  {
    arr,
    key,
    salt = '',
    isAllType = false,
    isRecursion = false,
    mode = TripleDesConst.MODE_DEFAULT,
    padding = TripleDesConst.PADDING_DEFAULT,
  }: {
    arr: any[] | null,
    isRecursion?: boolean,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
) => {
  // 如果 arr 为 null，则直接返回
  if (arr === null) return null
  // 对数组中的每项数据进行加密处理
  return arr.map((item) => {
    return encryptItemRecursion({
      item, key, salt, isAllType, isRecursion, mode, padding
    })
  })
}

/**
 * 对对象类型的数据进行加密处理<br/>
 * 该方法会循环对对象中的每项数据进行加密处理，然后返回一个由加密后的数据项组成的新对象
 *
 * @param obj 需要加密处理的对象数据。
 * @param key 加密密钥
 * @param salt 加密盐(偏移量)
 * @param isAllType 是否对所有的数据类型进行加密，默认为 false。<span style="color:orange;">
 * 该项开启后，对于对象中不是字符串类型的每一项数据，会将其转化为 JSON 字符串，然后进行加密处理；如果
 * 没有开启，对于不为字符串类型的数据项，则不会进行加密处理，仍然原数据项</span>
 * @param isRecursion 是否递归处理对象项。默认为 false。<span style="color:orange;">如果开启了
 * 递归加密处理，则对于数组类型或者对象类型的数据项，会优先进行递归，然后再对其中的子数组项进行加密处理
 * </span>
 * @param mode 加密模式。默认为 ECB
 * @param padding 最后一个分组的填充方式。默认为 ZeroPadding
 */
export const encryptObj = (
  {
    obj,
    key,
    salt = '',
    isAllType = false,
    isRecursion = false,
    mode = TripleDesConst.MODE_DEFAULT,
    padding = TripleDesConst.PADDING_DEFAULT,
  }: {
    obj: { [prop: string]: any } | null,
    isRecursion?: boolean,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
) => {
  // 如果对象为 null，则直接返回
  if (obj === null) return null
  // 用于保存加密后数据的对象
  let newObj: { [prop: string]: any } = {}
  // 遍历对象属性，对每项数据进行加密处理
  for (let prop in obj) {
    let item = obj[prop]
    newObj[prop] = encryptItemRecursion({
      item, key, salt, isAllType, isRecursion, mode, padding
    })
  }
  return newObj
}

/**
 * 对任意类型的数据进行加密处理<br/>
 *
 * @param obj 需要加密处理的对象数据。
 * @param key 加密密钥
 * @param salt 加密盐(偏移量)
 * @param isAllType 是否对所有的数据类型进行加密，默认为 false。<span style="color:orange;">
 * 该项开启后，对于对象中不是字符串类型的每一项数据，会将其转化为 JSON 字符串，然后进行加密处理；如果
 * 没有开启，对于不为字符串类型的数据项，则不会进行加密处理，仍然原数据项</span>
 * @param isRecursion 是否递归处理对象项。默认为 true。<span style="color:orange;">如果开启了
 * 递归加密处理，则对于数组类型或者对象类型的数据项，会优先进行递归，然后再对其中的子数组项进行加密处理
 * </span>
 * @param mode 加密模式。默认为 ECB
 * @param padding 最后一个分组的填充方式。默认为 ZeroPadding
 * @return {any} 加密处理后的数据
 */
export const encryptAny = (
  {
    data,
    key,
    salt = '',
    isAllType = false,
    isRecursion = true,
    mode = TripleDesConst.MODE_DEFAULT,
    padding = TripleDesConst.PADDING_DEFAULT,
  }: {
    data: any,
    isRecursion?: boolean,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
): any => {
  return encryptItemRecursion({
    item: data,
    key,
    salt,
    isAllType,
    isRecursion,
    mode,
    padding
  })
}

/**
 * DES 加密算法解密函数<br/>
 *
 * @param data 需要进行解密处理的数据。<span style="color:orange;">如果需要解密处理的数据为空，
 * 或者不是字符串类型且没有开启对所有的数据类型进行解密，或者为空字符串，都将不进行解密处理，直接返回传
 * 递进来的数据</span>
 * @param key 加密密钥。<span style="color:orange;">如果密钥长度小于 24 位，则会直接返回传递进
 * 来需要解密处理的密文；如果密钥长度大于 24 位，则会对其进行截取，只取前 24 位</span>
 * @param salt 加密盐(偏移量)。<span style="color:orange;">如果加密盐为空或长度小于 8 位，则会
 * 使用密钥的前 8 位作为盐；如果加密盐长度大于 8 位，则会对其进行截取，只取前 8 位</span>
 * @param isAllType 加密处理时，是否对所有的数据类型进行加密，默认为 false。
 * <span style="color:orange;">该项开启后，对于解密后不是字符串类型的数据，会对其进行 JSON 字符串
 * 解析；如果没有开启，对于传入的需要解密处理的数据为空，或者不是字符串类型，或者为空字符串，都将不进行解
 * 密处理，直接返回传递进来的数据</span>
 * @param mode 加密模式。默认为 ECB
 * @param padding 最后一个分组的填充方式。默认为 ZeroPadding
 */
export const decrypt = (
  {
    data,
    key,
    salt = '',
    isAllType = false,
    mode = TripleDesConst.MODE_DEFAULT,
    padding = TripleDesConst.PADDING_DEFAULT,
  }: {
    data: any,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
) => {
  // 如果传递进来的数据不需要后续的解密处理，直接返回原数据
  if (!isDes(data, key, isAllType as boolean)) return data
  // 预处理密钥和加密盐
  let {key: handledKey, salt: handledSalt} = preHandleKeySalt(key, salt as string)
  // 解密处理
  let plainText = CryptoJS.enc.Utf8.stringify(
    CryptoJS.TripleDES.decrypt(
      data, // 需要进行解密处理的密文
      CryptoJS.enc.Utf8.parse(handledKey), // 密钥
      {
        iv: CryptoJS.enc.Utf8.parse(handledSalt), // 偏移量(加密盐)
        mode: TripleDesMode[mode], // 加密模式
        padding: TripleDesPadding[padding], // 填充方式
      }
    )
  )
  // 处理解密后的明文
  return handleData(plainText, isAllType as boolean, true)
}

/**
 * 递归解密处理对象类型或者数组类型数据中的数据项
 *
 * @param item 需要解密处理的数据项
 * @param key 密钥
 * @param salt 加密盐(偏移量)
 * @param isAllType 加密处理时，是否对所有的数据类型进行加密
 * @param isRecursion 是否递归处理数据项
 * @param mode 加密模式
 * @param padding 最后一个分组的填充方式
 */
const decryptItemRecursion = (
  {
    item, key, salt, isAllType, isRecursion, mode, padding
  }: {
    item: any,
    isRecursion?: boolean,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
) => {
  // 如果数据项为 null，则直接返回
  if (item === null) return null
  // 如果是对象类型的数据，且需要递归处理
  if (typeof item === 'object' && isRecursion) {
    // 递归处理数组
    if (Array.isArray(item)) item = decryptArr({
      arr: item,
      key, salt, isAllType, isRecursion, mode, padding,
    })
    // 递归处理对象
    else item = decryptObj({
      obj: item,
      key, salt, isAllType, isRecursion, mode, padding,
    })
  } else {
    item = decrypt({
      data: item,
      key, salt, isAllType, mode, padding,
    })
  }
  return item
}

/**
 * 对数组类型的数据进行解密处理<br/>
 * 该方法会循环对数组中的每项数据进行解密处理，然后返回一个由解密后的数据项组成的新数组
 *
 * @param arr 需要解密处理的数组数据。
 * @param key 密钥
 * @param salt 加密盐(偏移量)
 * @param isAllType 加密处理时，是否对所有的数据类型进行加密，默认为 false。
 * <span style="color:orange;">该项开启后，对于解密后不是字符串类型的数据项，会对其进行 JSON 字符
 * 串解析；如果没有开启，对于不是字符串类型的数据项，都将不进行解密处理，直接保留原数据项</span>
 * @param isRecursion 是否递归处理数组项。默认为 false。<span style="color:orange;">如果开启
 * 了递归解密处理，则对于数组类型或者对象类型的数据项，会优先进行递归，然后再对其中的子数组项进行解密处理
 * </span>
 * @param mode 加密模式。默认为 ECB
 * @param padding 最后一个分组的填充方式。默认为 ZeroPadding
 */
export const decryptArr = (
  {
    arr,
    key,
    salt = '',
    isAllType = false,
    isRecursion = false,
    mode = TripleDesConst.MODE_DEFAULT,
    padding = TripleDesConst.PADDING_DEFAULT,
  }: {
    arr: any[] | null,
    isRecursion?: boolean,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
) => {
  // 如果 arr 为 null，则直接返回
  if (arr === null) return null
  // 对数组中的每项数据进行解密处理
  return arr.map((item) => {
    return decryptItemRecursion({
      item, key, salt, isAllType, isRecursion, mode, padding
    })
  })
}

/**
 * 对对象类型的数据进行解密处理<br/>
 * 该方法会循环对对象中的每项数据进行解密处理，然后返回一个由解密后的数据项组成的新对象
 *
 * @param obj 需要解密处理的对象数据。
 * @param key 密钥
 * @param salt 加密盐(偏移量)
 * @param isAllType 加密处理时，是否对所有的数据类型进行加密，默认为 false。
 * <span style="color:orange;">该项开启后，对于解密后不是字符串类型的数据项，会对其进行 JSON 字符
 * 串解析；如果没有开启，对于不是字符串类型的数据项，都将不进行解密处理，直接保留原数据项</span>
 * @param isRecursion 是否递归处理数组项。默认为 false。<span style="color:orange;">如果开启
 * 了递归解密处理，则对于数组类型或者对象类型的数据项，会优先进行递归，然后再对其中的子数组项进行解密处理
 * </span>
 * @param mode 加密模式。默认为 ECB
 * @param padding 最后一个分组的填充方式。默认为 ZeroPadding
 */
export const decryptObj = (
  {
    obj,
    key,
    salt = '',
    isAllType = false,
    isRecursion = false,
    mode = TripleDesConst.MODE_DEFAULT,
    padding = TripleDesConst.PADDING_DEFAULT,
  }: {
    obj: { [prop: string]: any } | null,
    isRecursion?: boolean,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
) => {
  // 如果需要解密处理的对象为 null，则直接返回
  if (obj === null) return null
  // 用于保存解密后数据的对象
  let newObj: { [prop: string]: any } = {}
  // 遍历对象属性，对每项数据进行解密处理
  for (let prop in obj) {
    let item = obj[prop]
    newObj[prop] = decryptItemRecursion({
      item, key, salt, isAllType, isRecursion, mode, padding
    })
  }
  return newObj
}

/**
 * 对任意类型的数据进行解密处理<br/>
 *
 * @param obj 需要解密处理的对象数据。
 * @param key 密钥
 * @param salt 加密盐(偏移量)
 * @param isAllType 加密处理时，是否对所有的数据类型进行加密，默认为 false。
 * <span style="color:orange;">该项开启后，对于解密后不是字符串类型的数据项，会对其进行 JSON 字符
 * 串解析；如果没有开启，对于不是字符串类型的数据项，都将不进行解密处理，直接保留原数据项</span>
 * @param isRecursion 是否递归处理数据项。默认为 true。<span style="color:orange;">如果开启
 * 了递归解密处理，则对于数组类型或者对象类型的数据项，会优先进行递归，然后再对其中的子数据项进行解密处理
 * </span>
 * @param mode 加密模式。默认为 ECB
 * @param padding 最后一个分组的填充方式。默认为 ZeroPadding
 * @returns {any} 解密处理后的数据
 */
export const decryptAny = (
  {
    secretData,
    key,
    salt = '',
    isAllType = false,
    isRecursion = true,
    mode = TripleDesConst.MODE_DEFAULT,
    padding = TripleDesConst.PADDING_DEFAULT,
  }: {
    secretData: any,
    isRecursion?: boolean,
    key: string,
    salt?: string,
    isAllType?: boolean
    mode?: TripleDesModeType,
    padding?: TripleDesPaddingType
  }
): any => {
  return decryptItemRecursion({
    item: secretData,
    key,
    salt,
    isAllType,
    isRecursion,
    mode,
    padding
  })
}
